﻿using Geometry;
using System;
using System.Collections.ObjectModel;
using System.Linq;

namespace UserInterface.Primitives
{
	public sealed class GridField : Collection<GridBand>
	{
        private float percentMultiplier;

		public GridField()
		{
		}

        internal float DesiredLength { get { return this.Sum(band => band.DesiredLength); } }

		#region Lanes Layout Calculation
		// 1) First reset to minumum sizes
		public void Reset()
		{
            this.percentMultiplier = 0;

			foreach (var band in this)
			{
                if (band.Unit == GridBandUnit.Points)
				{
					band.InitialDesiredLength = band.Length;
					band.AdditionalDesiredLength = 0;
				}
				else
				{
					band.InitialDesiredLength = band.Minimum;
					band.AdditionalDesiredLength = 0;
				}

                band.AvailableLength = 0f;
                band.End = 0f;
			}

            int nextAuto = 1;
            int nextPercent = 1;
            for (int i = this.Count - 1; i >= 0; i--)
            {
                GridBand band = this[i];
                nextAuto++;
                nextPercent++;
                switch (band.Unit)
                {
                    case GridBandUnit.Auto:
                        nextAuto = 0;
                        break;
                    case GridBandUnit.Percent:
                        nextPercent = 0;
                        break;
                }
                band.NextAuto = nextAuto;
                band.NextPercent = nextPercent;
            }
		}

		// 2) Then add items to Autos
		internal void PushSingleBandAutoItem(int index, float size)
		{
			var band = this[index];
            if (band.Unit == GridBandUnit.Auto)
			{
				band.InitialDesiredLength = Math.Max(band.InitialDesiredLength, Math.Max(size, band.Minimum));
			}
		}

		// 3) prepare for spanning items - order the lanes by available span

		// 4) Stretch for spanning items.
		internal void PushSpanningAutoItem(int index, int span, float size)
		{
            if (this.Skip(index).Take(span).Any(band => band.Unit == GridBandUnit.Percent))
			{
				return;
			}

			var bands = this.Skip(index).Take(span).ToList();
			size -= bands.Sum(s => s.InitialDesiredLength);

			if (size <= 0)
			{
				return;
			}

			bands = bands.OrderBy(AvailableSpan).ToList();

			for (int i = span; i >= 1; i--)
			{
				var band = bands[i - 1];

				float even = size / i;
				float available = AvailableSpan(band);

				if (available <= even)
				{
					size -= available;
					band.AdditionalDesiredLength = Math.Max(band.AdditionalDesiredLength, available);
				}
				else
				{
					size -= even;
					band.AdditionalDesiredLength = Math.Max(band.AdditionalDesiredLength, even);
				}
			}
		}

        // 6) Stretch for percent items.
        internal void PushPercentItem(int index, int span, float size)
        {
            var multiplier = CalculatePercentMultiplier(index, span, size);
            this.percentMultiplier = Math.Max(this.percentMultiplier, multiplier);
        }

        private float CalculatePercentMultiplier(int index, int span, float size)
        {
            float multiplier = 0;

            var items = this.Skip(index).Take(span);
            size -= items.Sum(l => l.DesiredLength);

            if (size > 0)
            {

                var minimums = items.Where(band => band.Unit == GridBandUnit.Percent).OrderBy(band => band.Minimum / band.Length).ToList();
                var maximums = items.Where(band => band.Unit == GridBandUnit.Percent).OrderBy(band => band.Maximum / band.Length).ToList();

                var minEnum = minimums.GetEnumerator();
                var maxEnum = maximums.GetEnumerator();

                if (minEnum.MoveNext() && maxEnum.MoveNext())
                {
                    var hasMinPoints = true;
                    var hasMaxPoints = true;

                    multiplier = 0f;
                    var percentMultiplier = 0f;
                    var accumulatedSize = 0f;
                    do
                    {
                        System.Diagnostics.Debug.Assert(!hasMinPoints || (minEnum.Current.Length > 0f && !float.IsNaN(minEnum.Current.Length) && !float.IsInfinity(minEnum.Current.Length)));
                        System.Diagnostics.Debug.Assert(!hasMaxPoints || (maxEnum.Current.Length > 0f && !float.IsNaN(maxEnum.Current.Length) && !float.IsInfinity(maxEnum.Current.Length)));
                        var minPoint = hasMinPoints ? minEnum.Current.Minimum / minEnum.Current.Length : 0;
                        var maxPoint = hasMaxPoints ? maxEnum.Current.Maximum / maxEnum.Current.Length : 0;

                        if (!hasMaxPoints || (hasMinPoints && hasMaxPoints && minPoint < maxPoint))
                        {
                            var gain = percentMultiplier * (minPoint - multiplier);

                            if (accumulatedSize <= size && accumulatedSize + gain >= size)
                            {
                                multiplier += (size - accumulatedSize) / percentMultiplier;
                                break;
                            }

                            accumulatedSize += gain;
                            multiplier = minPoint;

                            percentMultiplier += minEnum.Current.Length;
                            hasMinPoints = minEnum.MoveNext();
                        }
                        else if (!hasMinPoints || (hasMinPoints && hasMaxPoints && minPoint >= maxPoint))
                        {
                            var gain = percentMultiplier * (maxPoint - multiplier);

                            if (accumulatedSize <= size && accumulatedSize + gain >= size)
                            {
                                multiplier += (size - accumulatedSize) / percentMultiplier;
                                break;
                            }

                            accumulatedSize += gain;
                            multiplier = maxPoint;

                            percentMultiplier -= minEnum.Current.Length;
                            hasMaxPoints = maxEnum.MoveNext();
                        }
                        else
                        {
                            // we have nothing left... probably...
                            System.Diagnostics.Debug.Assert(!hasMinPoints && !hasMaxPoints);
                            break;
                        }
                    }
                    while (true);
                }
            }

            return multiplier;
        }

        internal void SetAvailableLengthForPercents(float length)
        {
            float constraintMultiplier = CalculatePercentMultiplier(0, this.Count, length);
            float end = 0f;
            foreach (var band in this)
            {
                var l = band.Unit == GridBandUnit.Percent ? Math.Max(band.Minimum, Math.Min(band.Maximum, band.Length * constraintMultiplier)) : band.DesiredLength;
                band.End = end += l;
            }
        }

        internal void SetArrangeLengthForPercents(float length)
        {
            foreach(var band in this.Where(band => band.Unit == GridBandUnit.Percent))
            {
                band.AdditionalDesiredLength = 0;
            }
            this.percentMultiplier = CalculatePercentMultiplier(0, this.Count, length);
            this.CalculatePercentsDesiredSize();
            float end = 0;
            for (int i = 0; i < this.Count; i++)
            {
                var band = this[i];
                band.End = end += band.DesiredLength;
            }
        }

        internal void CalculatePercentsDesiredSize()
        {
            foreach (var item in this.Where(band => band.Unit == GridBandUnit.Percent))
            {
                item.AdditionalDesiredLength = Math.Max(item.Minimum, Math.Min(item.Maximum, item.Length * this.percentMultiplier)) - item.InitialDesiredLength;
            }
        }

        internal bool IsSpanAuto(int index, int length)
        {
            GridBand band = this[index];
            bool isAuto = band.NextAuto < length && band.NextPercent >= length;
            return isAuto;
        }

        internal float GetAvailableSpan(int index, int length)
        {
            var start = index == 0 ? 0 : this[index - 1].End;
            var end = this[index + length - 1].End;
            return end - start;
        }

        internal Interval GetArrangeInterval(int index, int length)
        {
            var start = index == 0 ? 0 : this[index - 1].End;
            var end = this[index + length - 1].End;
            return new Interval(start, end);
        }

		private static float AvailableSpan(GridBand band)
		{
            return band.Unit == GridBandUnit.Auto ? band.Maximum - band.InitialDesiredLength : 0f;
		}
		#endregion

		#region Lanes Ownership
		protected override void ClearItems()
		{
			foreach (var lane in this)
			{
				lane.GridLayout = null;
			}

			base.ClearItems();
		}

		protected override void InsertItem(int index, GridBand band)
		{
			if (band == null)
			{
				throw new ArgumentNullException("lane");
			}

			if (band.GridLayout != null)
			{
				throw new InvalidOperationException("GridLane allready a child of a GridLayout.");
			}

			base.InsertItem(index, band);
			band.GridLayout = this;
		}

		protected override void RemoveItem(int index)
		{
			GridBand band = this[index];
			base.RemoveItem(index);
			band.GridLayout = null;
		}

		protected override void SetItem(int index, GridBand band)
		{
			var previousLane = this[index];

			if (previousLane == band)
			{
				return;
			}

			if (band == null)
			{
				throw new ArgumentNullException("lane");
			}

			if (band.GridLayout != null)
			{
				throw new InvalidOperationException("GridLane allready a child of a GridLayout.");
			}

			base.SetItem(index, band);
			previousLane.GridLayout = null;
			band.GridLayout = this;
		}
		#endregion
    }
}
